﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace AutoFakes
{
    using System.IO;
    using System.Linq;
    using System.Management.Instrumentation;

    public class AutoStub<T> where T : class
    {
        private readonly Assembly _callingAssembly;
        private readonly List<object> _fakes = new List<object>();

        public AutoStub()
        {
            if (Target == null)
            {
                _callingAssembly = Assembly.GetCallingAssembly();
                InitializeTarget();
            }
        }

        public T Target { get; private set; }

        private void InitializeTarget()
        {
            ConstructorInfo bestCtor = FindCtorWithMostParams();
            
            object[] stubParams = BuildParams(bestCtor);

            Target = (T)bestCtor.Invoke(stubParams);
        }

        private static ConstructorInfo FindCtorWithMostParams()
        {
            return typeof (T).GetConstructors().MaxBy(x => x.GetParameters().Count());
        }

        private object[] BuildParams(ConstructorInfo constructors)
        {
            var parameterInfoArray = constructors.GetParameters();

            return parameterInfoArray.Select(BuildParam).ToArray();
        }

        private object BuildParam(ParameterInfo paramInfo)
        {
            if (paramInfo.ParameterType.IsAbstract || paramInfo.ParameterType.IsInterface)
                return BuildStubParam(paramInfo);

            throw new InvalidOperationException("Dependency was not of a type that can be created automatically.");
        }

        private object BuildStubParam(ParameterInfo paramInfo)
        {
            var loadedAssemblies = GetLoadedAssemblies();

            string stubClassName = "Stub" + paramInfo.ParameterType.Name;
            Type stubTypeInfo = FindMatchingStubInLoadedAssemblies(stubClassName, loadedAssemblies) ??
                                FindMatchingStubInReferencedAssemblies(stubClassName, loadedAssemblies);

            if (stubTypeInfo == null)
                throw new DependencyNotStubbedException("Abstract Class or Interface has not been stubbed.  Are you missing a Fake?");

            var stubCtor = stubTypeInfo.GetConstructors()[0];
            var stubObject = stubCtor.Invoke(null);

            _fakes.Add(stubObject);
            
            return stubObject;
        }

        private static List<Assembly> GetLoadedAssemblies()
        {
            var assemblyList = new List<Assembly>(AppDomain.CurrentDomain.GetAssemblies());
            assemblyList.Insert(0, Assembly.GetExecutingAssembly());
            return assemblyList;
        }

        private Type FindMatchingStubInReferencedAssemblies(string stubName, IEnumerable<Assembly> assemblies)
        {
            var stubType = new Type[] {};
            var assemblyNameInfo = _callingAssembly.GetReferencedAssemblies();
            
            foreach (var module in GetModules(assemblyNameInfo, assemblies))
            {
                stubType = module.FindTypes(Module.FilterTypeName, stubName);
                if (stubType.Length > 0)
                    break;
            }

            return stubType.Length > 0 ? stubType[0] : null;
        }

        private static Type FindMatchingStubInLoadedAssemblies(string stubName, IEnumerable<Assembly> assemblies)
        {
            var stubType = new Type[] {};
            foreach (var module in GetLoadedModules(assemblies))
            {
                stubType = module.FindTypes(Module.FilterTypeName, stubName);
                if (stubType.Length > 0)
                    break;
            }
            return stubType.Length > 0 ? stubType[0] : null;
        }

        private static IEnumerable<Module> GetLoadedModules(IEnumerable<Assembly> assemblies)
        {
            return assemblies.SelectMany(assembly => assembly.GetModules(false));
        }

        private static IEnumerable<Module> GetModules(IEnumerable<AssemblyName> assemblyNameInfo, IEnumerable<Assembly> assemblies)
        {
            return from assemblyName in assemblyNameInfo
                   where AssemblyNotLoaded(assemblyName, assemblies)
                   select Assembly.Load(assemblyName)
                   into newAssembly from module in newAssembly.GetModules(false) select module;
        }

        private static bool AssemblyNotLoaded(AssemblyName assemblyName, IEnumerable<Assembly> assemblyList)
        {
            return assemblyList.All(assembly => assemblyName.FullName != assembly.FullName);
        }

        public T1 Get<T1>() where T1 : class
        {
            return Get<T1>(0);
        }

        public T1 Get<T1>(int index) where T1 : class
        {
            int cntFound = 0;
            foreach (var fake in _fakes)
                if (fake as T1 != null )
                    if (cntFound == index)
                        return (T1)fake;
                    else
                        cntFound++;
            
            if (cntFound == 0)
                throw new KeyNotFoundException();
            throw new InvalidDataException("Invalid Index");
        }
    }
}